#include <cserver/server.h>
#include <cserver/data.h>
#include <cserver/db.h>
#include <cserver/context.h>
#include <cserver/session/node.h>

#include <boost/asio.hpp>
#if 1
#include <boost/asio/io_service.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/write.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/lexical_cast.hpp>
//#include <boost/asio/ssl.hpp>
#endif

#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/log/trivial.hpp>

#include <thread>
#include <iostream>
#include <string>
#include <array>
#include <chrono>
#include <iomanip>
#include <vector>
#include <sstream>
#include <unordered_map>
#include <cstdlib>


namespace csrv{
	namespace {
		const std::string K_client_res{R"({"result":{"code":200, "msg":"success"}})"};

#pragma pack(push, 1)
		union SizeHead{
			uint16_t size;
			std::uint8_t data[sizeof(uint16_t)];
		};
#pragma pack(pop)
	}

	std::unordered_map<std::string, uint8_t> K_CommandTable{
		{"light_pwm", 0x24},
			{"report_intervel", 0x50},
	};

	//using namespace boost::asio;
	using namespace boost::asio::ip;
	class ServerImp{
		public:
			Context& context;

			tcp::acceptor acceptor;
			tcp::acceptor echo_acceptor;
			tcp::acceptor node_acceptor;
			boost::asio::steady_timer read_deadline;

			int mSeqno = 0;
		public:
			ServerImp(Context& ctx) 
				: context(ctx)
				  , acceptor(context.ios_client, tcp::endpoint(address::from_string(ctx.config.cserver_addr), ctx.config.cserver_port)) 
				  , echo_acceptor(context.ios_client, tcp::endpoint(address::from_string(ctx.config.echo_addr), ctx.config.echo_port)) 
				  , node_acceptor(context.ios_client, tcp::endpoint(address::from_string(ctx.config.node_addr), ctx.config.node_port)) 
				  , read_deadline(context.ios_aserver)
		{
		}

			void start(){
				context.database->writeTest("Server created.");
				BOOST_LOG_TRIVIAL(info) << "Server started";
				start_accept_();
				start_accept_nbiot_();
			}

			void start_accept_(){
				boost::asio::spawn(context.ios_aserver, [&](boost::asio::yield_context yield) { 
						for(;;){try{do_accept_(yield);return;}catch(...){}}});
			}

			void start_accept_nbiot_(){
				boost::asio::spawn(context.ios_aserver, [&](boost::asio::yield_context yield) { 
						for(;;){try {do_accept_echo_(yield); return;}catch(...){}}});
				boost::asio::spawn(context.ios_aserver, [&](boost::asio::yield_context yield) { 
						for(;;){try {do_udp_echo_(yield); return;}catch(...){}}});
				boost::asio::spawn(context.ios_aserver, [&](boost::asio::yield_context yield) { 
						for(;;){try {do_accept_node_(yield); return;}catch(...){}}});
			}

			void do_accept_(boost::asio::yield_context yield){
				for(;acceptor.is_open();){
					boost::system::error_code ec;
					auto socket = std::make_shared<tcp::socket>(context.ios_client);
					acceptor.async_accept(*socket, yield[ec]);
					if (!ec)
						boost::asio::spawn(context.ios_client, [socket, this](boost::asio::yield_context yield_) { 
								for(;;){try{ run_session(socket, yield_); return;}catch(...){}} });
				}
			}
			void do_accept_node_(boost::asio::yield_context yield){
				boost::system::error_code ec;
				for(;node_acceptor.is_open();){
					auto socket = std::make_shared<tcp::socket>(context.ios_client);
					node_acceptor.async_accept(*socket, yield[ec]);
					if (!ec){
						auto node_tcp = std::make_shared<NodeTcp>(context, socket);
						boost::asio::spawn(context.ios_client, [node_tcp](boost::asio::yield_context yield_) { 
								for(;;){try{ node_tcp->start(yield_);  return;}catch(...){}}});
					}
				}
			}
			void do_accept_echo_(boost::asio::yield_context yield){
				boost::system::error_code ec;
				for(;echo_acceptor.is_open();){
					auto socket = std::make_shared<tcp::socket>(context.ios_client);
					echo_acceptor.async_accept(*socket, yield[ec]);
					if (!ec){
						boost::asio::spawn(context.ios_client, [socket, this](boost::asio::yield_context yield_) { 
								for(;;){try{ run_echo_(socket, yield_);  return;}catch(...){}}});
					}
				}
			}

			void do_udp_echo_(boost::asio::yield_context yield)
			{
				std::vector<char> buffer(65536);
				boost::system::error_code ec;
				udp::socket socket(context.ios_aserver, udp::endpoint(address::from_string(context.config.echo_addr), context.config.echo_port)); 
				for(;socket.is_open();){
					udp::endpoint sender_endpoint_;
					auto size = socket.async_receive_from(boost::asio::buffer(buffer, buffer.size()), sender_endpoint_,yield[ec]);
					if (!ec) 
					{
						BOOST_LOG_TRIVIAL(info) <<"UDP ECHO: " << std::string(buffer.begin(), buffer.begin() + size);
						socket.async_send_to(boost::asio::buffer(buffer, size), sender_endpoint_, yield[ec]);	
					}
				}
			}
			void run_echo_(std::shared_ptr<tcp::socket> socket, boost::asio::yield_context yield){
				std::vector<char> buffer(65536);
				boost::system::error_code ec;
				while (socket->is_open()){
					auto size = socket->async_read_some(boost::asio::buffer(buffer, buffer.size()), yield[ec]);
					if (ec) break;
					BOOST_LOG_TRIVIAL(info) <<"TCP ECHO: " << std::string(buffer.begin(), buffer.begin() + size);
					boost::asio::async_write(*socket, boost::asio::buffer(buffer, size), yield[ec]);	
					if (ec) break;
				}
			}
			void run_session(std::shared_ptr<tcp::socket> socket, boost::asio::yield_context yield){
				while (socket->is_open()){
					boost::system::error_code ec;

					SizeHead size;
					boost::asio::async_read(*socket, boost::asio::buffer(size.data), yield[ec]);
					if (ec)	return;
					std::swap(size.data[0], size.data[1]);	// from Big endian to little endian

					std::vector<char> buf(size_t(size.size));
					boost::asio::async_read(*socket, boost::asio::buffer(buf), yield[ec]);
					if (ec)	return;
					std::string data(buf.begin(), buf.end());
					if (ec || buf.size() <= 6) continue;	//heartbeat

					BOOST_LOG_TRIVIAL(info) << "Client Data:" << data;

					try{
						std::stringstream sstr(data);
						boost::property_tree::ptree pt;
						read_json(sstr, pt);

						bool has_command = false;
						for(auto& item : pt.get_child("cmds")){
							has_command = true;
							auto cmd_str = item.second.get_child("cmd").data();
							auto pos = K_CommandTable.find(cmd_str);
							if (pos == K_CommandTable.end()) continue;
							auto cmd = pos->second;

							std::string parameter = item.second.get_child("parameter").data();
							for(auto& euid : item.second.get_child("euids")){
								auto json = make_app_json(boost::lexical_cast<uint64_t>(euid.second.data()), cmd, parameter);
								context.loraUpstream->append(std::vector<char> (json.begin(), json.end()));
							}
						}
						if (has_command){
							size.size = uint16_t(K_client_res.size());
							std::swap(size.data[0], size.data[1]);
							boost::asio::async_write(*socket, boost::asio::buffer(size.data), yield[ec]);	
							boost::asio::async_write(*socket, boost::asio::buffer(K_client_res), yield[ec]);	
							if (ec){
								BOOST_LOG_TRIVIAL(warning) <<"Failed to send to client.";
								break;
							}
						}
					}catch(...){
						BOOST_LOG_TRIVIAL(warning) <<"Bad command:" << data ;
						return;
					}
				}
			}

			std::string make_app_json(uint64_t euid, uint8_t cmd, const std::string& param){

				auto value_param = boost::lexical_cast<uint32_t>(param);
				std::vector<uint8_t> buf;
				buf.push_back(cmd);
				switch(cmd){
					case 0x24:	// light pwm
						buf.push_back(0x01);
						value_param = std::min(value_param, uint32_t(100));
						value_param = value_param * 0xfe / 100;
						buf.push_back(uint8_t(value_param));
						break;
					case 0x50:	// report interval
						buf.push_back(0x02);
						value_param = std::min(value_param, uint32_t(0xffff));
						buf.push_back(uint8_t(value_param & 0xff));
						buf.push_back(uint8_t(value_param >> 8));
						break;
				}
				auto payload = base64_encode(reinterpret_cast<char*>(&buf[0]), buf.size());
				std::stringstream sstr;
				sstr << R"({"app":{" "moteeui":)" << euid << ","
					<< R"("dir":"dn", "token":1, "confirmed":false, "userdata":{ "seqno":)"
					<< mSeqno << R"(,"port":1, "payload":")" << payload 
					<< R"(", "motetx":null}, "gwrx":null)"
					<<"}}";

				return sstr.str();
			}
			};

			Server::Server(Context& ctx)
				: m_imp(new ServerImp(ctx))
			{}
			Server::~Server(){}

			void Server::start(){ m_imp->start();}
			ServerPtr Server::make(Context& ctx){
				return std::make_shared<Server>(ctx);
			}
	}
